大家好!在 Day 22,我們透過建立中央訊息服務,讓 App 的使用者體驗更加精緻。今天,我們將目光轉回主畫面的核心——那張寫著 NT$ 12,345
的「本月總支出」卡片。從 Day 5 建立至今,它一直是一個靜態的假數據,是時候讓它真正地「動」起來了。
今天的目標是:串接 Firestore 資料,即時計算並顯示使用者「當前月份」的總消費金額。我們將利用 Stream
的強大能力,讓總金額能隨著每筆消費的新增或刪除自動更新,使其成為使用者最直觀的財務儀表板。
要計算本月總支出,首先需要從 Firestore 中篩選出屬於「當前月份」的消費紀錄。這需要在查詢時加入時間範圍的過濾條件。
邏輯很簡單:我們需要找出「本月第一天的 00:00:00」和「下個月第一天的 00:00:00」,然後查詢所有 date
欄位介於這兩個時間點之間的紀錄。
在 Dart 中,我們可以這樣取得這兩個時間點:
final now = DateTime.now();
// 取得本月第一天
final startOfMonth = DateTime(now.year, now.month, 1);
// 取得下個月第一天 (月份+1,日期設為1)
final endOfMonth = DateTime(now.year, now.month + 1, 1);
既有的 getTransactionsStream
會抓取所有紀錄,不符需求。因此,我們在 lib/services/firestore_service.dart
中新增一個專門的方法,用來獲取當前月份的交易串流。
// lib/services/firestore_service.dart
// ...
class FirestoreService {
// ... 其他方法 ...
// Day 23 新增方法:取得當前月份的交易紀錄串流
Stream<QuerySnapshot> getCurrentMonthTransactionsStream({required String userId}) {
final now = DateTime.now();
final startOfMonth = DateTime(now.year, now.month, 1);
final endOfMonth = DateTime(now.year, now.month + 1, 1);
return _usersCollection
.doc(userId)
.collection('transactions')
// 篩選條件:date >= 本月第一天
.where('date', isGreaterThanOrEqualTo: Timestamp.fromDate(startOfMonth))
// 篩選條件:date < 下個月第一天
.where('date', isLessThan: Timestamp.fromDate(endOfMonth))
.snapshots();
}
}
程式碼解析
.where()
方法來增加篩選條件。DateTime
物件轉換為 Timestamp
格式。現在,我們回到主畫面 lib/main.dart
,用 StreamBuilder
來包裹總支出卡片,讓它訂閱我們剛剛建立的資料流,並即時計算總和。
import 'package:cloud_firestore/cloud_firestore.dart' hide Transaction;
// lib/main.dart -> _HomePageState -> build()
@override
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
return Scaffold(
// ... AppBar 和 FloatingActionButton ...
body: Column(
children: [
// --- 核心改造開始 ---
// 1. 用 StreamBuilder 包裹總支出卡片
StreamBuilder<QuerySnapshot>(
// 2. stream 指向我們的新方法
stream: user != null
? _firestoreService.getCurrentMonthTransactionsStream(userId: user.uid)
: null,
builder: (context, snapshot) {
// 處理載入中
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
// 處理錯誤
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
// 處理沒有資料
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
// 即使沒資料,也顯示 0 元,而不是不顯示卡片
return _buildTotalSpentCard(0.0);
}
// 3. 計算總和
double totalSpent = 0.0;
for (var doc in snapshot.data!.docs) {
final data = doc.data() as Map<String, dynamic>;
// 確保 amount 欄位存在且為數字型別
if (data.containsKey('amount') && data['amount'] is num) {
totalSpent += data['amount'];
}
}
// 4. 使用計算出的 totalSpent 來建立卡片
return _buildTotalSpentCard(totalSpent);
},
),
// --- 核心改造結束 ---
// ... 中間功能按鈕區塊 ...
// ... 列表標題 ...
Expanded(
// ... 顯示交易列表的 StreamBuilder (維持不變) ...
),
],
),
);
}
// 5. 將原本的 Container 抽離成一個獨立的輔助函式,方便重用
Widget _buildTotalSpentCard(double totalSpent) {
return Container(
padding: const EdgeInsets.all(24.0),
margin: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'本月總支出',
style: TextStyle(fontSize: 16, color: Colors.black54),
),
Text(
'NT\$ ${totalSpent.toStringAsFixed(0)}', // 顯示計算出的總金額
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: Colors.teal.shade700,
),
),
],
),
);
}
StreamBuilder
的 stream
指向我們在 FirestoreService
中建立的新方法 getCurrentMonthTransactionsStream
。builder
回呼函式中,我們先處理載入、錯誤與無資料等狀態。snapshot
提供的文件,即時計算出總金額。build
方法更清晰,我們將卡片的 UI 抽離成一個 _buildTotalSpentCard
獨立的輔助函式。重啟 App,你會發現總支出卡片已能顯示 Firestore 中當前月份的真實總和。試著新增或刪除一筆消費,數字會神奇地即時更新!
今天我們完成了 App 核心功能的最後一項動態化。至此,「省錢拍拍」已從一個概念,成長為功能完整、架構清晰,並具備 AI 能力的雲端應用。
接下來的一週,我們將進行專案的收尾工作:
flutter_launcher_icons
和 flutter_native_splash
產生自訂的 App 圖示與啟動畫面。今天我們讓主畫面的儀表板真正活了起來,重點如下:
StreamBuilder
進行即時的數據聚合運算(加總)。我們的 App 現在不僅智慧,數據也完全即時同步。從明天開始,讓我們一起為 App 的「上市」做最後的準備吧!